Introduction to Custom Functions in Python

Because copy-pasting code is so last semester

February 07, 2025

Hello, everyone!
Nice to see you all! 😉

Lecture outline

  • Why Functions?
    • Code Repetition & DRY principle
  • What are Functions?
    • Reusable Blocks (def)
  • Function Syntax
    • Anatomy & Examples
  • Function Arguments
    • Positional, Keyword, Default
  • Lambda Functions
    • Anonymous, One-liners
  • Variable Scope
    • Local & Global Scope
  • Best Practices & Pitfalls
    • Naming, Docstrings, Indentation, return
  • Summary & Q&A
    • Recap & Next Steps

Let’s get started! 🚀

Why functions?

Why functions?

  • The problem: code repetition is a trap!
    • Imagine writing the same code…
    • … again and again and again! 😩
  • An example: greeting many people
    • Imagine doing this for 1000 names!
  • Error-prone updates
  • Hard to read and maintain
  • Not efficient at all! 🐢
# Greeting people (without functions)
# Don't do this at home!

print("Nice to meet you, Alice!")
print("Nice to meet you, Bob!")
print("Nice to meet you, Charlie!")
print("Nice to meet you, Danilo!")
print("Nice to meet you, Emily!")
print("Nice to meet you, Frank!")
print("Nice to meet you, George!")

# ... repeats 993 more times ...

Maybe there’s a better way to do this? 🤔

Functions to the rescue! 🦸‍♂️

The best way to greet people (and much more!)

  • Functions are like mini-programs:
    • Self-contained blocks of code
    • Designed to perform a specific task
  • “Recipes” for code: 🧑‍🍳
    • You define the steps (code inside the function)
    • You can “call” the function (use the recipe) whenever you need it
  • You know them already!
    • print(), type(), np.mean(), plt.hist() are all functions you’ve used before! 🤓
  • Why are they useful?
    • Reusable code
    • Organised logic
    • One change updates all
# Greeting people (with functions)
# Much better!

def greet(your_name):
    print(f"Nice to meet you, {your_name}!")

greet("Alice")
greet("Bob")
greet("Danilo")
Nice to meet you, Alice!
Nice to meet you, Bob!
Nice to meet you, Danilo!

DRY principle: Don’t Repeat Yourself 😉

Anatomy of a Python function

The building blocks

  • Writing a function is like writing a sentence: it requires some structure! 🤓📚

  1. def keyword. Tells Python: “We are defining a function now”
  2. Function Name (function_name). Choose a descriptive name (e.g., greet, calculate_age, etc)
  3. Parentheses (). Enclose inputs the function needs. Empty () means no inputs
  4. Parameter(s) (optional) (parameter). A placeholder for the values the function will receive. Can be multiple, separated by commas
  1. Argument(s) (optional) (parameter=argument). The actual values that will be passed to the function.
  2. Colon : Ends the function header. Don’t forget it!
  3. Function Body. Code to execute. After four spaces!
  4. return statement (optional). What the function sends back. If absent, function returns None. Indent it too!

Let’s see some examples together 🛠️

Hello, Alice and Bob!

  • Function with one parameter: say_greeting(name)
def say_greeting(name):
    print(f"Hello, {name}! Nice to see you.")
  • Think about it:
    • Can you identify the parts we discussed?
    • Does it have a return statement?
    • What if we store the output in a variable, like message_alice = say_greeting("Alice")?
say_greeting("Alice")
Hello, Alice! Nice to see you.
message_alice = say_greeting("Alice")
Hello, Alice! Nice to see you.
type(message_alice)
NoneType

Let’s see some examples together 🛠️

Hello, Alice and Bob!

  • And what about this one?
def get_greeting(name, greeting_type="Hello"):
    message = f"{greeting_type}, {name}! Nice to see you."
    return message
  • Think about it:
    • How is this function different from say_greeting?
    • What about the greeting_type="Hello" part? What does it mean?
    • What if you run: message_bob = get_greeting("Bob") and then print(message)?
# No errors and no output!
message_bob = get_greeting("Bob")
type(message_bob)
str
print(message_bob)
Hello, Bob! Nice to see you.

We can use the result in further calculations!

More about the return statement here

Passing arguments to functions

Positional vs keyword arguments

  • We pass arguments to functions to make them flexible
  • There are two ways to pass arguments: positional and keyword arguments
  • Positional arguments are passed in the order the parameters are defined
  • Let’s see an example:
def get_greeting(name, greeting_type="Hello"):
    message = f"{greeting_type}, {name}! Nice to see you."
    return message

message = get_greeting("Alice", "Hi")
print(message)
Hi, Alice! Nice to see you.

Positional arguments
  • What happens if you switch the order of the arguments?
message = get_greeting("Hi", "Alice")
print(message)
Alice, Hi! Nice to see you.

Order matters! 🚨

Keyword arguments

  • If you use keyword arguments, you can pass arguments by name in any order 😉
  • This is useful when you have many arguments or default values, so you don’t need to remember which ones come first
message = get_greeting(greeting_type="Hi", name="Alice")
print(message)
Hi, Alice! Nice to see you.
  • Now it works just fine! 🎉

  • The same is true for many functions you use often:

import numpy as np

# Default order: loc, scale, size. But you can use keywords!
vector = np.random.normal(size = 4, loc = 10, scale = 2)
print(vector)
[ 9.35205944 10.08977232  6.15552857  9.34796644]
  • So far, so good? 🤓

Let’s create a function together 🛠️

  • Time to put our knowledge to the test! 🚀
  • Let’s create a function that calculates the area of a rectangle
  • Please complete the calculate_area function below 👇
def calculate_area(_____, _____):   # What parameters do we need?
    # Function body - calculate the area
    ____ = _____ * _____           # How do we calculate area? Assign to 'area' variable
    return ____                    # What should the function return?
  • Done? Then, call the function with length=5 and width=4, and print the result
result = calculate_area(length=5, width=4)    # Call with arguments
print(result)                                # Check if it's correct!

Another one? 🤓

Oops, something may be wrong here!

  • This function is almost identical to the previous one
  • It looks fine, right? Try to run it and see what happens!
def greet_incorrect(greeting_type="Hello", name):
    message = f"{greeting_type}, {name}!"
    return message
  • What’s the error message? 🚨
  Cell In[14], line 1
    def greet_incorrect(greeting_type="Hello", name): 
                                               ^
SyntaxError: parameter without a default follows parameter with a default
  • What went wrong?
    • Python requires non-default arguments first
    • Fix it by moving name before greeting_type
    • I told you that order matters! 😂
  • Let’s fix it together:
def greet_correct(name, greeting_type="Hello"):
    message = f"{greeting_type}, {name}!"
    return message

message = greet_correct("Alice", "Hi")
print(message)
Hi, Alice!

Phew, that was a lot!

Questions? 😉

Lambda (anonymous) functions 🕵🏽‍♀️

Lambda functions

  • Sometimes you don’t need a full function definition
  • Just a simple, one-line function is enough
  • Lambda functions are perfect for this!
    • They can have many arguments but only one expression (no return statement)
    • They are also known as anonymous functions, but can be assigned to a variable
  • Syntax: lambda arguments: expression
    • lambda x, y: x + y
  • Let’s calculate x + y + z using a lambda function:
  • Similar to writing a regular function, but in one line!
sum_function = lambda x, y, z: x + y + z

result = sum_function(1, 2, 3)
print(result)
6
  • Easy, right?
  • Now have a look at this 🤯
result = (lambda x, y, z: x + y + z)(1, 2, 3)
print(result)
6

Try it yourself! 🛠️

NASA needs your help! 🚀

  • Exercise: Create a lambda function that calculates travel_distance using the formula speed * time
  • Use the lambda function to calculate the distance for speed = 100 and time = 2
  • Print the result


Great job, everyone! 🎉

Global vs local variables 🌍

Local variables

What happens inside functions stays inside functions! 🤫

  • Python has a concept called variable scope
  • Scope means: Where in your code can you use a variable? 🤔
  • First, let’s understand local variables:
    • They are called local because they are “local” to the function
    • They only exist and can be used within that function’s body
    • They are not accessible from outside the function
  • Think of it like a private workspace:
    • But once the function finishes, that workspace (and its local variables) disappears!
def my_function():
    # message is a local variable
    message = "Hello from inside the function!"
    print(message)

# This works!
my_function() 
Hello from inside the function!
  • Try to access message from outside:
print(message) # Error!
NameError: name 'message' is not defined
  • message has disappeared!
  • Is it clear why? 🤓

Global variables

The world is your oyster (but use them sparingly!) 🌍

  • Now, let’s look at global variables:
    • Variables defined outside of any function (at the top level of your code) are called global variables
    • They can be accessed from anywhere in your code, including inside functions!
  • Think of it like shared information for your whole program:
    • Any part of your code can see and use global variables
# 'global_greeting' is defined outside
global_greeting = "Welcome" 

# Accessing global variable inside
def greet_function(name):
    print(f"{global_greeting} {name}") 

greet_function("Alice") # Works fine!
print(global_greeting)   # Works fine outside too!
Welcome Alice
Welcome
  • What happens if you try to change a global variable inside a function?
global_greeting = "Welcome"

def change_greeting(new_greeting):
    global_greeting = new_greeting

change_greeting("Hello!") # No error!
print(global_greeting)    # But it doesn't change!
Welcome
  • What’s going on here?

The global keyword

When you need to change a global variable inside a function

  • If you want to change a global variable inside a function, you need to use the global keyword

  • This tells Python: “Hey, I’m talking about the global variable here!”

  • Let’s see an example:

global_greeting = "Welcome!"

def change_greeting(new_greeting):
    global global_greeting
    global_greeting = new_greeting

change_greeting("Hello!")
print(global_greeting) # Now it works!
Hello!
  • That’s why we need to be careful about using global variables inside functions
  • They can make your code hard to understand and debug
  • Did you notice how global_greeting is not passed as an argument to the function?
  • And yet we can change its value inside the function!
  • So beware! 🚨

Best practices and pitfalls 🚧

Best practices

How to write better functions

  • Python has a style guide called PEP 8 (link here)
  • It provides best practices for writing clean, readable code
  • Here are some tips for writing better functions
  • Including some I have learnt the hard way (don’t ask me how)! 😅
  • Naming conventions:
    • Use descriptive names for functions and variables
    • snake_case for functions and variables (camelCase is fine too)
    • Avoid special characters and reserved words (e.g., $, @, print, sum, list)
  • Bad: def f(x):, def function1():, def sum():, def cR@zY_fUnC(): 👎
  • Good: def calculate_area():, def greet_user():, def download_wbi(): 👍

Best practices

Use docstrings to document your functions! 📚

  • Docstrings are multi-line comments that describe what your function does
  • They are enclosed in triple quotes (""" or ''')
  • They should include:
    • Function purpose
    • Parameters and return values
    • Additional information if needed
  • They help others (and you!) understand your code
  • Good documentation is a great way to ensure your code is maintainable
def calculate_area(length, width):
    """
    Calculate the area of a rectangle.

    Parameters:
    length (int): The length of the rectangle.
    width (int): The width of the rectangle.

    Returns:
    int: The area of the rectangle.
    """
    area = length * width
    return area
  • That’s beautiful, isn’t it? 😄

Pitfalls to avoid! 🚧

Indentation errors, missing return, and more!

  • You all know that Python is indentation-sensitive
  • It’s not to make your code look pretty, but to define the structure of your code
  • Incorrect indentation = IndentationError!
    • Python will complain loudly if your indentation is wrong 😅
  • Common mistake 1: Forgetting to indent the function body
# No indentation!
def my_function():
print("Hello!")

# Good code!
def my_function():
    print("Hello!")
  • Common mistake 2: Incorrect number of spaces
# Bad code! 2 spaces
def my_function():
  print("Hello!")

# Good code! 4 spaces!
def my_function():
    print("Hello!")
  • Common mistake 3: Mixing spaces and tabs
# Mixing spaces and tabs
def my_function():
        print("Using 4 tabs")
    print("Using 4 spaces")

# Good code! Use spaces only!
def my_function():
    print("Using 4 spaces")

Pitfalls to avoid! 🚧

Return and print: Know the difference! 🤔

  • Return and print are not the same!
  • Return sends a value back to the caller
  • Print displays a value on the screen
  • Common mistake 1: Using print instead of return
def double(x):
    print(x * 2)

# Good code! Use return!
def double(x):
    return x * 2
  • Common mistake 2: Forgetting to return a value when you need one
def double(x):
    x * 2

# Good code! Don't forget to return!
def double(x):
    return x * 2
  • Common mistake 3: Using one return inside a loop or if statement
# No return statement for false?
def is_even(number):
    if number % 2 == 0:
        return True

# Good code! Return outside the if statement
def is_even(number):
    if number % 2 == 0:
        return True
    return False

Summary 📚

Summary

  • Functions are mini-programs that perform a specific task
  • They help you avoid code repetition and keep your code clean
  • Function anatomy:
    • def keyword, name, parameters, body, return, and indentation
    • Positional vs keyword arguments
    • Default values
  • Global vs local variables:
    • Local variables are only accessible inside the function
    • Global variables can be accessed from anywhere
    • Use global keyword to change global variables inside functions
  • Best practices:
    • Naming conventions
    • Docstrings
    • Indentation
    • Return vs print
  • Pitfalls to avoid:
    • Indentation errors
    • Missing return statements
    • Global vs local variables
    • Using print instead of return
    • Using return inside loops or if statements
    • Mixing spaces and tabs

But wait, there’s more! 😅

Further reading

Thank you very much! 🙏

…and we’re back! 😄

Current and prospective courses

QTM 151 - Introduction to Statistical Computing II

Data Analysis with Python and SQL

  • The course introduces students to data analysis with Python and SQL
  • It covers:
    • Data manipulation with Pandas and NumPy
    • Data visualisation with Matplotlib
    • Writing and running functions
    • Time series and panel data analysis
    • Introduction to SQL for data retrieval
    • Jupyter Notebooks and reproducible research
  • All materials are free and openly available

Click on the image to see the website

Course website: https://danilofreire.github.io/qtm151/

QTM 350 - Data Science Computing

Modern Data Science and Engineering

  • The course introduces students to modern data science and engineering, focusing on reliability, reproducibility, and robustness
  • The course covers:
    • Command line interface
    • Version control with git and GitHub (using the terminal)
    • Literate programming with Quarto
    • AI-assisted programming
    • Introduction to cloud computing
    • Data storage and retrieval with SQL
    • Parallel computing with Dask
    • Containers and reproducible workflows with Docker
  • All materials are also free and openly available

Click on the image to see the website

Course website: https://danilofreire.github.io/qtm350/

QTM 385 - Experimental Methods

  • QTM 385 is a course on experimental methods in the social and health sciences
  • The main topics covered are:
    • The logic of causal inference
    • Randomisation procedures
    • Blocking and clustering
    • Pre-analysis plans and registered reports
    • Power calculations
    • Non-compliance and attrition
    • Survey experiments
    • Research ethics
  • As with the other courses, all materials are available online

Click on the image to see the website

Course website: https://danilofreire.github.io/qtm385/

Suggested Course: Small Language Models

How to build and fine-tune your own SLMs

  • Small Language Models are more useful than we think:
    • Efficient: Run on local machines, minimal GPU
    • Customisable: Tailor-made for your specific needs
    • Controlled: Understand and influence model behaviour
    • Accessible: Lower cost, increased privacy
  • Hands-on learning with practical skills:
    • No complex setups, just your laptop and open-source tools
    • Focus on real-world applications and contributing to the community
    • Based on the Hugging Face ecosystem (transformers, trl, datasets), SMoLM2, and ollama

Suggested Course: Small Language Models

How to build and fine-tune your own SLMs

  • Key topics covered:

Questions? 😉

Thanks again! 😃

Appendix 01: Area of a rectangle

  • Here’s the solution to the exercise:
def calculate_area(length, width):
    area = length * width
    return area

result = calculate_area(5, 4)
print(result)
20

Back to the function

Appendix 02: Travel distance

  • Here’s the solution to the exercise:
travel_distance = lambda speed, time: speed * time

distance = travel_distance(100, 2)
print(f"The distance is: {distance}")
The distance is: 200
  • Or:
distance = (lambda speed, time: speed * time)(100, 2)
print(f"The distance is: {distance}")
The distance is: 200

Back to the function

Appendix 03: Nested functions

  • You can also define functions inside other functions:
  • This is called a nested function
  • They work like regular functions, but are only accessible inside the outer function
def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function(5)
  • Here we are using the inner function to add 5 to x
  • Pretty cool, right? 🤓

Appendix 04: Functions with for loops

  • You can also use for loops inside functions
  • They’re useful when you need to repeat a task multiple times
def sum_list(numbers):
    """
    Calculates the sum of numbers in a list.
    """
    total = 0
    for number in numbers:
        total += number
    return total

# Example usage:
numbers = [1, 2, 3, 4, 5]
result = sum_list(numbers)
print(result)
15

Appendix 05: LEGB rule

  • The LEGB rule is a way to understand how Python looks for variables
  • It stands for:
    • Local: Variables defined inside a function
    • Enclosing: Variables in the local scope of enclosing functions
    • Global: Variables defined at the top level of a module
    • Built-in: Predefined names in Python (e.g., print, len)
  • Python will look for a variable in the local scope first, then in the enclosing functions, then in the global scope, and finally in the built-in scope
def outer_function():
    x = 10
    def inner_function():
        y = 5
        return x + y
    return inner_function()

print(outer_function())
15
  • Here, x is in the enclosing scope of inner_function, so it can access it

Appendix 06: *args: Packing Positional Arguments

  • Let’s see something a bit more advanced! 😅
  • *args and **kwargs are great tools for writing flexible functions

*args: Packing Positional Arguments

  • What if you want a function that accepts a variable number of positional arguments?
  • *args has got you covered! It:
    • Collects extra positional arguments into a tuple called args
    • Allows your function to be called with any number of positional arguments
  • Think of it as: *args = “arguments” (with a star to make it special!)
def make_pizza(*toppings): 
    print("Making a pizza with the following toppings:")
    for topping in toppings: 
        print(f"- {topping}")

make_pizza("pepperoni", "mushrooms")
Making a pizza with the following toppings:
- pepperoni
- mushrooms
make_pizza("cheese", "onions", "olives", "sausage")
Making a pizza with the following toppings:
- cheese
- onions
- olives
- sausage
make_pizza() # Still works!
Making a pizza with the following toppings:

Appendix 07: **kwargs: Packing Keyword Arguments

  • Similarly, **kwargs handles a variable number of keyword arguments!
  • Collects extra keyword arguments into a dictionary called kwargs
def user_profile(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

user_profile(name="Alice", age=30, city="Wonderland")
name: Alice
age: 30
city: Wonderland
user_profile(occupation="Coder", hobby="Python", level="Beginner")
occupation: Coder
hobby: Python
level: Beginner
  • Inside the function, kwargs is a dictionary! Access arguments by key
  • Importanrt: *args must come before **kwargs in the function definition

Appendix 08: Error handling

  • Error handling is a way to manage errors that may occur during the execution of your code
  • It helps you handle unexpected situations gracefully
  • You can use try and except blocks to catch and handle errors
  • Imagine accessing items in a list… but what if you ask for too much? 🤔
def get_list_item_safe(my_list, index):
    """
    Safely gets an item from a list at a given index,
    handling IndexError.
    """
    try:
        item = my_list[index]
        return item
    except IndexError:
        return "Error: Index is out of range!"

my_list = ["apple", "banana", "cherry"]

print(get_list_item_safe(my_list, 1))  
print(get_list_item_safe(my_list, 5))  
banana
Error: Index is out of range!
  • except IndexError: catches errors when the index is invalid for the list!

  • Without try...except, your program would crash with an IndexError if you tried to access an invalid index.

  • Think of it as a safety net for your code!

  • This is especially useful when:

    • Dealing with user input (which might be unpredictable)
    • Working with external data (which might have unexpected formats)